Skip to content

Do not use std::aligned_storage#47

Merged
alt-graph merged 10 commits into
mainfrom
cleanup/do_not_use_std_aligned_storage
May 27, 2026
Merged

Do not use std::aligned_storage#47
alt-graph merged 10 commits into
mainfrom
cleanup/do_not_use_std_aligned_storage

Conversation

@alt-graph
Copy link
Copy Markdown
Member

[why]

std::aligned_storage is deprecated since C++23, see: https://stackoverflow.com/questions/71828288/why-is-stdaligned-storage-to-be-deprecated-in-c23-and-what-to-use-instead

[how]

Use alignas() for the internal buffer and specify the alignment explicitly when calling new for an allocated buffer. As it turns out, an aligned new[] has to be deallocated with an aligned delete[], so we have to change that handling, too. We also had no unit test for the alignment requirements, so we add one.

alt-graph added 3 commits May 21, 2026 15:57
[why]
We should verify that SmallVector actually places its elements according
to their alignment requirements. As it turns out, it does. :)
[why]
std::aligned_storage is deprecated since C++23, see:
https://stackoverflow.com/questions/71828288/why-is-stdaligned-storage-to-be-deprecated-in-c23-and-what-to-use-instead

[how]
Use alignas() for the internal buffer and specify the alignment
explicitly when calling new for an allocated buffer. As it turns out,
an aligned new[] has to be deallocated with an aligned delete[].
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates gul17::SmallVector to avoid using std::aligned_storage (deprecated in C++23) by switching to an alignas(...)-aligned internal byte buffer and using aligned new[] / delete[] for heap storage. It also adds a unit test to validate alignment after growth and shrink_to_fit().

Changes:

  • Replace std::aligned_storage with an alignas(ValueType) internal std::byte buffer and aligned heap allocation/deallocation.
  • Add an alignment regression test that exercises push_back() growth and shrink_to_fit() transitions.
  • Update changelog/copyright headers.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
include/gul17/SmallVector.h Reworks internal/heap storage to avoid std::aligned_storage and uses aligned new[]/delete[].
tests/test_SmallVector.cc Adds a new template test to verify element alignment and contiguity across growth/shrink.
data/doxygen.h Updates the “Unreleased” changelog section.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread include/gul17/SmallVector.h Outdated
Comment thread include/gul17/SmallVector.h Outdated
Comment thread include/gul17/SmallVector.h Outdated
Comment thread tests/test_SmallVector.cc
Comment thread data/doxygen.h
[why]
The function could leave data_ptr_ dangling after the call. This was
clearly documented and logically OK within the class, but could raise
suspicion.

[how]
Explicitly document that the function may leave the class in a state in
which the class invariants are violated. If deallocation takes place,
assign nullptr to data_ptr_ so that there is a better chance to find
misuses.
@alt-graph alt-graph marked this pull request as draft May 22, 2026 08:07
@alt-graph alt-graph marked this pull request as ready for review May 22, 2026 08:25
alt-graph added 3 commits May 22, 2026 10:39
[why]
MSVC mistakes the statement

allocation = new (alignment) std::byte[new_capacity * sizeof(ValueType)];

for a placement new even if alignment is of the type std::align_val_t,
and therefore generates a compilation error.

[how]
Use the low-level form of new directly like this:

auto* ptr = static_cast<std::byte*>(
    ::operator new[](num_elements * sizeof(ValueType), alignment));

Because this is ugly, extract both allocation and deallocation into
static member functions.
[why]
Having a private member function that breaks the class invariants may be
confusing, and it does not make things any clearer than having the two
lines directly in the code.
@alt-graph alt-graph force-pushed the cleanup/do_not_use_std_aligned_storage branch from f3533f0 to 7c0ab98 Compare May 22, 2026 08:40
Copy link
Copy Markdown
Member

@Finii Finii left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not too familiar with this alignment stuff, esp because I'm used to x86 which is so forgiving with alignment).
But I guess this looks good.

I have some minor comments, though.

And maybe we should run this at least on arm (do we?) because arm does not tolerate alignment errors and bus-errors out (instead of just creating a runtime penalty as on x86).
Apart from the hardware platform I also have no clue how (older) MSVC handle this 🤔

Comment thread tests/test_SmallVector.cc
Comment thread include/gul17/SmallVector.h Outdated
Comment thread include/gul17/SmallVector.h
@Finii
Copy link
Copy Markdown
Member

Finii commented May 26, 2026

And maybe we should run this at least on arm

According to https://github.com/actions/runner-images macos-latest (which we use) is ARM 👍

@alt-graph
Copy link
Copy Markdown
Member Author

And maybe we should run this at least on arm (do we?) because arm does not tolerate alignment errors and bus-errors out (instead of just creating a runtime penalty as on x86).

Good idea. It builds and tests fine on MacOS-aarch64. ✅

Apart from the hardware platform I also have no clue how (older) MSVC handle this 🤔

It also builds and tests fine on my "ancient" x86-64 Visual Studio 2019 installation. ✅

@Finii
Copy link
Copy Markdown
Member

Finii commented May 26, 2026

What I do not really understand, esp after reading the linked SO thread, by do we stick to std::byte at all instead of

-    alignas(ValueType)
-    std::array<std::byte, in_capacity * sizeof(ValueType)> internal_array_;
-        
-    /// Pointer to internal or external contiguous data storage.
-    std::byte* data_ptr_{ internal_array_.data() };
-
-    static std::byte* allocate_space_for_elements(std::size_t num_elements)
-    {
-        return static_cast<std::byte*>(                  
-            ::operator new[](num_elements * sizeof(ValueType), alignment));
-    }

+    std::array<ValueType, in_capacity> internal_array_;
+       
+    /// Pointer to internal or external contiguous data storage.
+    ValueType* data_ptr_{ internal_array_.data() };
+
+    static ValueType* allocate_space_for_elements(std::size_t num_elements)
+    {
+        return static_cast<ValueType*>(                  
+            ::operator new[](num_elements * sizeof(ValueType), alignment));
+    }

And then we can drop all the reinterpret_casts later on? 🤔
Why stick to void* / std::byte instead using the concrete type whenever possible?

[why]
The code actually becomes more readable by using the expressions
alignof(ValueType) or std::align_val_t{ alignof(ValueType) } directly.
@Finii
Copy link
Copy Markdown
Member

Finii commented May 26, 2026

6501d7f

NICE

[how]
If we store data_ptr as a ValueType* instead of std::byte* and access
the internal array only via an accessor function
get_internal_array_pointer(), we can limit the need for reinterpret_cast
to that accessor function (the internal array is still an array of
std::byte).

Proposed-by: Fini Jastrow <ulf.fini.jastrow@desy.de>
@alt-graph
Copy link
Copy Markdown
Member Author

What I do not really understand, esp after reading the linked SO thread, by do we stick to std::byte at all instead of

-    alignas(ValueType)
-    std::array<std::byte, in_capacity * sizeof(ValueType)> internal_array_;
-        
-    /// Pointer to internal or external contiguous data storage.
-    std::byte* data_ptr_{ internal_array_.data() };
-
-    static std::byte* allocate_space_for_elements(std::size_t num_elements)
-    {
-        return static_cast<std::byte*>(                  
-            ::operator new[](num_elements * sizeof(ValueType), alignment));
-    }

+    std::array<ValueType, in_capacity> internal_array_;
+       
+    /// Pointer to internal or external contiguous data storage.
+    ValueType* data_ptr_{ internal_array_.data() };
+
+    static ValueType* allocate_space_for_elements(std::size_t num_elements)
+    {
+        return static_cast<ValueType*>(                  
+            ::operator new[](num_elements * sizeof(ValueType), alignment));
+    }

And then we can drop all the reinterpret_casts later on? 🤔 Why stick to void* / std::byte instead using the concrete type whenever possible?

Because I was too lazy. ;)

Yes, I guess it is worth it. Commit a3f3662 removes most of the reinterpret_casts. The internal array still has to stay an array of std::byte, though.

[why]
It makes the code more compact and more readable.
@alt-graph
Copy link
Copy Markdown
Member Author

@Finii: I have added two more commits after your approval. Please let me know if you want to have another look.

@Finii
Copy link
Copy Markdown
Member

Finii commented May 26, 2026

@Finii: I have added two more commits after your approval. Please let me know if you want to have another look.

Both look very good 👍

I struggle a bit with understanding the const correctness of get_internal_array_pointer() but my tries with more test cases just left me more confused; so I think it is all good 😁

@alt-graph
Copy link
Copy Markdown
Member Author

Thanks a lot, @Finii !

@alt-graph alt-graph merged commit f86b5af into main May 27, 2026
3 checks passed
@alt-graph alt-graph deleted the cleanup/do_not_use_std_aligned_storage branch May 27, 2026 06:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants